<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2">
<meta name="theme-color" content="#222">
<meta name="generator" content="Hexo 4.2.0">
  <link rel="apple-touch-icon" sizes="180x180" href="/images/favicon.png">
  <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon.png">
  <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon.png">
  <link rel="mask-icon" href="/images/favicon.png" color="#222">

<link rel="stylesheet" href="/css/main.css">


<link rel="stylesheet" href="/lib/font-awesome/css/font-awesome.min.css">
  <link rel="stylesheet" href="//cdn.jsdelivr.net/gh/fancyapps/fancybox@3/dist/jquery.fancybox.min.css">


<script id="hexo-configurations">
  var NexT = window.NexT || {};
  var CONFIG = {
    hostname: new URL('https://codz-me.vercel.app').hostname,
    root: '/',
    scheme: 'Muse',
    version: '7.7.1',
    exturl: false,
    sidebar: {"position":"right","display":"post","padding":18,"offset":12,"onmobile":false},
    copycode: {"enable":false,"show_result":false,"style":null},
    back2top: {"enable":true,"sidebar":false,"scrollpercent":false},
    bookmark: {"enable":false,"color":"#222","save":"auto"},
    fancybox: true,
    mediumzoom: false,
    lazyload: false,
    pangu: false,
    comments: {"style":"tabs","active":null,"storage":true,"lazyload":false,"nav":null},
    algolia: {
      appID: '',
      apiKey: '',
      indexName: '',
      hits: {"per_page":10},
      labels: {"input_placeholder":"Search for Posts","hits_empty":"We didn't find any results for the search: ${query}","hits_stats":"${hits} results found in ${time} ms"}
    },
    localsearch: {"enable":false,"trigger":"auto","top_n_per_article":1,"unescape":false,"preload":false},
    path: '',
    motion: {"enable":true,"async":false,"transition":{"post_block":"fadeIn","post_header":"slideDownIn","post_body":"slideDownIn","coll_header":"slideLeftIn","sidebar":"slideUpIn"}}
  };
</script>

  <meta name="description" content="OpenTSDB 简介OpenTSDB是一个基于HBase的可扩展时间序列数据库，支持数百万每秒的读写，与OpenTSDB的交互主要是通过运行一个或多个TSD来实现的。每个TSD是独立的，没有主设备，没有共享状态，因此您可以根据需要运行任意数量的 TSD，以处理向其施加的任何负载。 Cloudera Express管理并运行了生产环境的 HDFS&#x2F;HBase&#x2F;ZooKeeper。 项目背景Open">
<meta property="og:type" content="article">
<meta property="og:title" content="OpenTSDB 查询优化">
<meta property="og:url" content="https://codz-me.vercel.app/2019/11/20/opentsdb-query-optimization/index.html">
<meta property="og:site_name" content="Code Is Poetry">
<meta property="og:description" content="OpenTSDB 简介OpenTSDB是一个基于HBase的可扩展时间序列数据库，支持数百万每秒的读写，与OpenTSDB的交互主要是通过运行一个或多个TSD来实现的。每个TSD是独立的，没有主设备，没有共享状态，因此您可以根据需要运行任意数量的 TSD，以处理向其施加的任何负载。 Cloudera Express管理并运行了生产环境的 HDFS&#x2F;HBase&#x2F;ZooKeeper。 项目背景Open">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://codz-me.vercel.app/2019/11/20/opentsdb-query-optimization/jetbrains_support.png">
<meta property="article:published_time" content="2019-11-20T02:48:49.000Z">
<meta property="article:modified_time" content="2020-03-10T02:50:49.000Z">
<meta property="article:author" content="laudukang">
<meta property="article:tag" content="opentsdb">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://codz-me.vercel.app/2019/11/20/opentsdb-query-optimization/jetbrains_support.png">

<link rel="canonical" href="https://codz-me.vercel.app/2019/11/20/opentsdb-query-optimization/">


<script id="page-configurations">
  // https://hexo.io/docs/variables.html
  CONFIG.page = {
    sidebar: "",
    isHome: false,
    isPost: true
  };
</script>

  <title>OpenTSDB 查询优化 | Code Is Poetry</title>
  






  <noscript>
  <style>
  .use-motion .brand,
  .use-motion .menu-item,
  .sidebar-inner,
  .use-motion .post-block,
  .use-motion .pagination,
  .use-motion .comments,
  .use-motion .post-header,
  .use-motion .post-body,
  .use-motion .collection-header { opacity: initial; }

  .use-motion .site-title,
  .use-motion .site-subtitle {
    opacity: initial;
    top: initial;
  }

  .use-motion .logo-line-before i { left: initial; }
  .use-motion .logo-line-after i { right: initial; }
  </style>
</noscript>

</head>

<body itemscope itemtype="http://schema.org/WebPage">
  <div class="container use-motion">
    <div class="headband"></div>

    <header class="header" itemscope itemtype="http://schema.org/WPHeader">
      <div class="header-inner"><div class="site-brand-container">
  <div class="site-meta">

    <div>
      <a href="/" class="brand" rel="start">
        <span class="logo-line-before"><i></i></span>
        <span class="site-title">Code Is Poetry</span>
        <span class="logo-line-after"><i></i></span>
      </a>
    </div>
  </div>

  <div class="site-nav-toggle">
    <div class="toggle" aria-label="切换导航栏">
      <span class="toggle-line toggle-line-first"></span>
      <span class="toggle-line toggle-line-middle"></span>
      <span class="toggle-line toggle-line-last"></span>
    </div>
  </div>
</div>


<nav class="site-nav">
  
  <ul id="menu" class="menu">
        <li class="menu-item menu-item-home">

    <a href="/" rel="section"><i class="fa fa-fw fa-home"></i>首页</a>

  </li>
        <li class="menu-item menu-item-about">

    <a href="/about/" rel="section"><i class="fa fa-fw fa-user"></i>关于</a>

  </li>
        <li class="menu-item menu-item-tags">

    <a href="/tags/" rel="section"><i class="fa fa-fw fa-tags"></i>标签</a>

  </li>
        <li class="menu-item menu-item-categories">

    <a href="/categories/" rel="section"><i class="fa fa-fw fa-th"></i>分类</a>

  </li>
        <li class="menu-item menu-item-archives">

    <a href="/archives/" rel="section"><i class="fa fa-fw fa-archive"></i>归档</a>

  </li>
        <li class="menu-item menu-item-commonweal">

    <a href="/404/" rel="section"><i class="fa fa-fw fa-heartbeat"></i>公益 404</a>

  </li>
  </ul>

</nav>
</div>
    </header>

    
  <div class="back-to-top">
    <i class="fa fa-arrow-up"></i>
    <span>0%</span>
  </div>
  <div class="reading-progress-bar"></div>


    <main class="main">
      <div class="main-inner">
        <div class="content-wrap">
          

          <div class="content">
            

  <div class="posts-expand">
      
  
  
  <article itemscope itemtype="http://schema.org/Article" class="post-block " lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="https://codz-me.vercel.app/2019/11/20/opentsdb-query-optimization/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/images/head.png">
      <meta itemprop="name" content="laudukang">
      <meta itemprop="description" content="Bug Not Found">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="Code Is Poetry">
    </span>
      <header class="post-header">
        <h1 class="post-title" itemprop="name headline">
          OpenTSDB 查询优化
        </h1>

        <div class="post-meta">
              <span class="post-meta-item">
                <span class="post-meta-item-icon">
                  <i class="fa fa-calendar-check-o"></i>
                </span>
                <span class="post-meta-item-text">更新于</span>
                <time title="修改时间：2020-03-10 10:50:49 10:50:49" itemprop="dateModified" datetime="2020-03-10T10:50:49+08:00">2020-03-10 10:50:49</time>
              </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="fa fa-folder-o"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/categories/OpenTSDB/" itemprop="url" rel="index">
                    <span itemprop="name">OpenTSDB</span>
                  </a>
                </span>
            </span>

          
            <span id="/2019/11/20/opentsdb-query-optimization/" class="post-meta-item leancloud_visitors" data-flag-title="OpenTSDB 查询优化" title="阅读次数">
                <span hidden class="leancloud-visitors-count"></span>
            </span>

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">

      
        <h2 id="OpenTSDB-简介"><a href="#OpenTSDB-简介" class="headerlink" title="OpenTSDB 简介"></a>OpenTSDB 简介</h2><p><code>OpenTSDB</code>是一个基于HBase的可扩展时间序列数据库，支持数百万每秒的读写，与OpenTSDB的交互主要是通过运行一个或多个TSD来实现的。每个TSD是独立的，没有主设备，没有共享状态，因此您可以根据需要运行任意数量的 TSD，以处理向其施加的任何负载。</p>
<p><code>Cloudera Express</code>管理并运行了生产环境的 HDFS/HBase/ZooKeeper。</p>
<h2 id="项目背景"><a href="#项目背景" class="headerlink" title="项目背景"></a>项目背景</h2><p>OpenTSDB存储了SDN设备间的流量数据，粒度包括：源IP、目的IP、上下行流量、服务类型、协议等。</p>
<p>流量统计接口按时间区间直接查询了OpenTSDB的原始数据，未经Rollup处理。</p>
<h2 id="查询瓶颈"><a href="#查询瓶颈" class="headerlink" title="查询瓶颈"></a>查询瓶颈</h2><p>客户BJ联通反馈某台<code>SDN</code>设备11月初的流量统计接口返回超时。测试发现其他设备流量统计正常，联通设备查询时候有如下错误日志：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">16:14:13.466 ERROR [RegionClient.exceptionCaught] - Unexpected exception from downstream on [id: 0xbbc6970f, &#x2F;172.31.250.10:50790 &#x3D;&gt; &#x2F;172.31.120.131:16020]</span><br><span class="line">com.stumbleupon.async.CallbackOverflowError: Too many callbacks in Deferred@1273979129(state&#x3D;PENDING, result&#x3D;null, callback&#x3D;net.opentsdb.tsd.HttpJsonSerializer$1DPsResolver@682fd571 -&gt; net.opentsdb.tsd.HttpJsonSerializer$1DPsResolver@3061522a -&gt; net.opentsdb.tsd.HttpJsonSerializer$1DPsResolver@27fac471 -&gt; net.opentsdb.tsd.HttpJsonSerializer$1DPsResolver@17a290df -&gt; net.opentsdb.tsd.HttpJsonSerializer$1DPsResolver@1304b07 -&gt; </span><br><span class="line">&#x2F;&#x2F;....</span><br><span class="line">passthrough -&gt; passthrough) (size&#x3D;16383) when attempting to add cb&#x3D;net.opentsdb.tsd.HttpJsonSerializer$1DPsResolver@29bbd6fc@700176124, eb&#x3D;passthrough@1260931085</span><br></pre></td></tr></table></figure>

<p>Github上有人2016年就遇到该错误，<a href="https://github.com/OpenTSDB/opentsdb/issues/839" target="_blank" rel="noopener">CallbackOverflowError with many tag values</a>，issue至今未关闭</p>
<p>翻阅了OpenTSDB报错代码段</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// com.stumbleupon:async:1.4.1</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">Deferred</span>&lt;<span class="title">T</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">short</span> MAX_CALLBACK_CHAIN_LENGTH = (<span class="number">1</span> &lt;&lt; <span class="number">14</span>) - <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//....</span></span><br><span class="line">    <span class="comment">//com.stumbleupon.async.Deferred#addCallbacks</span></span><br><span class="line">    <span class="keyword">public</span> &lt;R, R2, E&gt; <span class="function">Deferred&lt;R&gt; <span class="title">addCallbacks</span><span class="params">(<span class="keyword">final</span> Callback&lt;R, T&gt; cb,</span></span></span><br><span class="line"><span class="function"><span class="params">                                               <span class="keyword">final</span> Callback&lt;R2, E&gt; eb)</span> </span>&#123;</span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (last_callback == callbacks.length) &#123;</span><br><span class="line">            <span class="keyword">final</span> <span class="keyword">int</span> oldlen = callbacks.length;</span><br><span class="line">            <span class="keyword">if</span> (oldlen == MAX_CALLBACK_CHAIN_LENGTH * <span class="number">2</span>) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> CallbackOverflowError(<span class="string">"Too many callbacks in "</span> + <span class="keyword">this</span></span><br><span class="line">                        + <span class="string">" (size="</span> + (oldlen / <span class="number">2</span>) + <span class="string">") when attempting to add cb="</span></span><br><span class="line">                        + cb + <span class="string">'@'</span> + cb.hashCode() + <span class="string">", eb="</span> + eb + <span class="string">'@'</span> + eb.hashCode());</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">final</span> <span class="keyword">int</span> len = Math.min(oldlen * <span class="number">2</span>, MAX_CALLBACK_CHAIN_LENGTH * <span class="number">2</span>);</span><br><span class="line">            <span class="keyword">final</span> Callback[] newcbs = <span class="keyword">new</span> Callback[len];</span><br><span class="line">            System.arraycopy(callbacks, next_callback,  <span class="comment">// Outstanding callbacks.</span></span><br><span class="line">                    newcbs, <span class="number">0</span>,            <span class="comment">// Move them to the beginning.</span></span><br><span class="line">                    last_callback - next_callback);  <span class="comment">// Number of items.</span></span><br><span class="line">            last_callback -= next_callback;</span><br><span class="line">            next_callback = <span class="number">0</span>;</span><br><span class="line">            callbacks = newcbs;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><code>MAX_CALLBACK_CHAIN_LENGTH</code>限制了<code>Deferred</code>链的长度，而进入到上面<code>addCallbacks</code>方法的为聚合分类后的每条数据，估计联通的问题就是处理数据超过了该限制。</p>
<p>测试也发现缩短联通设备的查询时间区间是能正常返回的，并且2019-11-06 8:00:00 - 2019-11-06 9:00:00的数据就近1.5W条返回，也就是说联通设备目的IP近1.5W个，大多数IP流量不多。</p>
<blockquote>
<p>Deferred类是Java库提供一些有用的构建模块去 构建高性能，多线程，异步的java应用。它的实现灵感来自Twisted的异步库（twisted.internet.defer）。<br>Deferred允许你轻松地构建异步的处理链，这个处理链必须触发，当一个异步的事件（I/O,RPC 以及其它）完成。它能被广泛用于构建异步API，在一个多线程服务器或者是客户端中。</p>
</blockquote>
<h2 id="着手解决"><a href="#着手解决" class="headerlink" title="着手解决"></a>着手解决</h2><p>OpenTSDB并不提供返回结果限制参数，至少2.4版本没有。</p>
<p>华为云CloudTable集成的OpenTSDB服务支持了结果限制，也就是query api提供了一个limit参数。</p>
<p>流量统计接口是统计该设备的<code>TOP10</code>流量，因此设置更大的MAX_CALLBACK_CHAIN_LENGTH没有意义：返回结果多，查询慢。</p>
<p>解决办法就是过滤进入到addCallback的数据大小：</p>
<p>1、OpenTSDB的查询最后准备返回结果的时候才通过三行代码过滤掉不在该时间区间的数据，</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//net/opentsdb/tsd/HttpJsonSerializer.java:835</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">final</span> DataPoint dp : dps) &#123;</span><br><span class="line">  <span class="keyword">if</span> (dp.timestamp() &lt; data_query.startTime() || </span><br><span class="line">      dp.timestamp() &gt; data_query.endTime()) &#123;</span><br><span class="line">    <span class="keyword">continue</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>因此一个优化就是在进入addCallback就过滤掉这一部分数据。</p>
<p>2、提供一个limit参数，限制返回结果集大小。这里分两步：一是进入addCallback前通过<code>TreeSet</code>自动排序，限制最多limit条数据会进入处理，二是OutputStream写入时候，排序后截取前limit条数据返回。</p>
<p>关键是提前过滤不在时间区间的数据点及TreeSet自动排序过滤掉大部分小流量的IP数据</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">HttpJsonSerializer</span> <span class="keyword">extends</span> <span class="title">HttpSerializer</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//net.opentsdb.tsd.HttpJsonSerializer#formatQueryAsyncV1</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Deferred&lt;ChannelBuffer&gt; <span class="title">formatQueryAsyncV1</span><span class="params">(<span class="keyword">final</span> TSQuery data_query,</span></span></span><br><span class="line"><span class="function"><span class="params">                                                      <span class="keyword">final</span> List&lt;DataPoints[]&gt; results, <span class="keyword">final</span> List&lt;Annotation&gt; globals)</span></span></span><br><span class="line"><span class="function">            <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> Deferred&lt;Object&gt; cb_chain = <span class="keyword">new</span> Deferred&lt;Object&gt;();</span><br><span class="line">        TreeSet&lt;Number&gt; numberTreeSet = LongStream.range(<span class="number">0</span> - limit, <span class="number">0</span>).mapToDouble(i -&gt; <span class="number">1.0</span> * i).boxed().collect(Collectors.toCollection(TreeSet::<span class="keyword">new</span>));</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (DataPoints[] separate_dps : results) &#123;</span><br><span class="line">            <span class="keyword">for</span> (DataPoints dps : separate_dps) &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="comment">//filter empty dps, laudukang</span></span><br><span class="line">                    <span class="keyword">boolean</span> isDpsOk = <span class="keyword">false</span>;</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">for</span> (<span class="keyword">final</span> DataPoint dp : dps) &#123;</span><br><span class="line">                        <span class="keyword">if</span> (dp.timestamp() &gt;= data_query.startTime() &amp;&amp;</span><br><span class="line">                                dp.timestamp() &lt;= data_query.endTime()) &#123;</span><br><span class="line">                            isDpsOk = <span class="keyword">true</span>;</span><br><span class="line">                            <span class="keyword">break</span>;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">if</span> (isDpsOk &amp;&amp; limit &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                        Number dpsSum = dps.sumDps();</span><br><span class="line">                        <span class="keyword">if</span> (numberTreeSet.first().doubleValue() &lt; dpsSum.doubleValue()) &#123;</span><br><span class="line">                            numberTreeSet.pollFirst();</span><br><span class="line">                            numberTreeSet.add(dpsSum.doubleValue());</span><br><span class="line">                        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                            <span class="comment">//filter</span></span><br><span class="line">                            isDpsOk = <span class="keyword">false</span>;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">if</span> (isDpsOk) &#123;</span><br><span class="line">                        cb_chain.addCallback(<span class="keyword">new</span> DPsResolver(dps));</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(<span class="string">"Unexpected error durring resolution"</span>, e);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>如query api传入参数包含limit，则先将数据写入到<code>ByteArrayOutputStream</code>（这样子源代码改动小），再从<code>ByteArrayOutputStream</code>中反序列化获得原响应数据，倒序并截取前limit条数据写入到<code>OutputStream</code></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//net.opentsdb.tsd.HttpJsonSerializer#sortDps</span></span><br><span class="line"><span class="meta">@SuppressWarnings</span>(<span class="string">"unchecked"</span>)</span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">sortDps</span><span class="params">(<span class="keyword">long</span> limit, ByteArrayOutputStream stream, OutputStream output)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">    <span class="keyword">byte</span>[] content = stream.toByteArray();</span><br><span class="line">    TypeReference&lt;List&lt;Map&lt;String, Object&gt;&gt;&gt; typeReference = <span class="keyword">new</span> TypeReference&lt;List&lt;Map&lt;String, Object&gt;&gt;&gt;() &#123;</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    List&lt;Map&lt;String, Object&gt;&gt; dataList = JSON.parseToObject(content, typeReference);</span><br><span class="line"></span><br><span class="line">    List&lt;Map&lt;String, Object&gt;&gt; resultMapList = dataList.stream()</span><br><span class="line">            .filter(m -&gt; !m.containsKey(<span class="string">"metric"</span>))</span><br><span class="line">            .collect(Collectors.toList());</span><br><span class="line"></span><br><span class="line">    List&lt;Map&lt;String, Object&gt;&gt; metricNodeMapList = dataList.stream()</span><br><span class="line">            .filter(m -&gt; m.containsKey(<span class="string">"metric"</span>))</span><br><span class="line">            .collect(Collectors.toList());</span><br><span class="line"></span><br><span class="line">    <span class="comment">//sum dps for sort</span></span><br><span class="line">    metricNodeMapList.forEach(m -&gt; &#123;</span><br><span class="line">        <span class="keyword">double</span> value = ((Map&lt;String, Object&gt;) m.get(<span class="string">"dps"</span>)).values().stream()</span><br><span class="line">                .mapToDouble(i -&gt; &#123;</span><br><span class="line">                    <span class="keyword">if</span> (i <span class="keyword">instanceof</span> Integer) &#123;</span><br><span class="line">                        <span class="keyword">return</span> ((Integer) i).doubleValue();</span><br><span class="line">                    &#125;</span><br><span class="line">                    <span class="keyword">return</span> (<span class="keyword">double</span>) i;</span><br><span class="line">                &#125;)</span><br><span class="line">                .sum();</span><br><span class="line"></span><br><span class="line">        m.put(<span class="string">"sumDps"</span>, value);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    Comparator&lt;Map&lt;String, Object&gt;&gt; dpsSorter = Comparator.comparingDouble(m -&gt; (<span class="keyword">double</span>) m.get(<span class="string">"sumDps"</span>));</span><br><span class="line">    List&lt;Map&lt;String, Object&gt;&gt; metricLimitNodeMapList = metricNodeMapList.stream()</span><br><span class="line">            .sorted(dpsSorter.reversed()).limit(limit)</span><br><span class="line">            .collect(Collectors.toList());</span><br><span class="line"></span><br><span class="line">    resultMapList.addAll(metricLimitNodeMapList);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">final</span> JsonGenerator json = JSON.getFactory().createGenerator(output);</span><br><span class="line">    json.writeObject(resultMapList);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="编译测试"><a href="#编译测试" class="headerlink" title="编译测试"></a>编译测试</h2><p>参考<a href="http://opentsdb.net/docs/build/html/development/development.html" target="_blank" rel="noopener">OpenTSDB General Development</a> <code>Compiles OpenTSDB and generates a Debian package</code>，卸载官方版本后安装。</p>
<p>修改 <a href="https://github.com/laudukang/opentsdb-okhttp-client" target="_blank" rel="noopener">laudukang/opentsdb-okhttp-client</a> 让其支持<code>limit</code>查询参数。</p>
<p>测试显示查询耗时明显缩短，而且响应大小也减少了，不再返回几MB的json数据。</p>
<p>查询例子<br><code>POST /api/query?summary</code></p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"limit"</span>: <span class="number">10</span>,<span class="comment">//限制返回条目数，倒序</span></span><br><span class="line">  <span class="attr">"delete"</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">"end"</span>: <span class="number">1573012800000</span>,</span><br><span class="line">  <span class="attr">"globalAnnotations"</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">"msResolution"</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">"noAnnotations"</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">"queries"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"aggregator"</span>: <span class="string">"sum"</span>,</span><br><span class="line">      <span class="attr">"downsample"</span>: <span class="string">"0all-sum"</span>,</span><br><span class="line">      <span class="attr">"explicitTags"</span>: <span class="literal">false</span>,</span><br><span class="line">      <span class="attr">"filters"</span>: [</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="attr">"filter"</span>: <span class="string">"*"</span>,</span><br><span class="line">          <span class="attr">"groupBy"</span>: <span class="literal">true</span>,</span><br><span class="line">          <span class="attr">"tagk"</span>: <span class="string">"dst_ip"</span>,</span><br><span class="line">          <span class="attr">"type"</span>: <span class="string">"wildcard"</span></span><br><span class="line">        &#125;,</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="attr">"filter"</span>: <span class="string">"CONNECTION_OVERSEAS"</span>,</span><br><span class="line">          <span class="attr">"groupBy"</span>: <span class="literal">false</span>,</span><br><span class="line">          <span class="attr">"tagk"</span>: <span class="string">"service_type"</span>,</span><br><span class="line">          <span class="attr">"type"</span>: <span class="string">"literal_or"</span></span><br><span class="line">        &#125;</span><br><span class="line">      ],</span><br><span class="line">      <span class="attr">"metric"</span>: <span class="string">"1164.conn.total.in"</span>,</span><br><span class="line">      <span class="attr">"preAggregate"</span>: <span class="literal">false</span>,</span><br><span class="line">      <span class="attr">"rate"</span>: <span class="literal">false</span>,</span><br><span class="line">      <span class="attr">"useMultiGets"</span>: <span class="literal">false</span></span><br><span class="line">    &#125;</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">"showQuery"</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">"showStats"</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">"showSummary"</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">"showTSUIDs"</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">"start"</span>: <span class="number">1572998400000</span>,</span><br><span class="line">  <span class="attr">"timezone"</span>: <span class="string">"UTC"</span>,</span><br><span class="line">  <span class="attr">"useCalendar"</span>: <span class="literal">false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>response</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">[</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">"metric"</span>: <span class="string">"1164.conn.total.in"</span>,</span><br><span class="line">    <span class="attr">"tags"</span>: &#123;</span><br><span class="line">      <span class="attr">"protocol"</span>: <span class="string">"TCP"</span>,</span><br><span class="line">      <span class="attr">"service_type"</span>: <span class="string">"CONNECTION_OVERSEAS"</span>,</span><br><span class="line">      <span class="attr">"dst_ip"</span>: <span class="string">"107.167.89.234"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">"aggregateTags"</span>: [</span><br><span class="line">      <span class="string">"src_ip"</span></span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">"dps"</span>: &#123;</span><br><span class="line">      <span class="attr">"157296960"</span>: <span class="number">17588304652</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">"sumDps"</span>: <span class="number">17588304652</span></span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">"metric"</span>: <span class="string">"1164.conn.total.in"</span>,</span><br><span class="line">    <span class="attr">"tags"</span>: &#123;</span><br><span class="line">      <span class="attr">"src_ip"</span>: <span class="string">"240.16.1.109"</span>,</span><br><span class="line">      <span class="attr">"protocol"</span>: <span class="string">"TCP"</span>,</span><br><span class="line">      <span class="attr">"service_type"</span>: <span class="string">"CONNECTION_OVERSEAS"</span>,</span><br><span class="line">      <span class="attr">"dst_ip"</span>: <span class="string">"152.199.39.180"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">"aggregateTags"</span>: [],</span><br><span class="line">    <span class="attr">"dps"</span>: &#123;</span><br><span class="line">      <span class="attr">"157296960"</span>: <span class="number">9804591660</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">"sumDps"</span>: <span class="number">9804591660</span></span><br><span class="line">  &#125;,</span><br><span class="line">  </span><br><span class="line">  <span class="comment">//余下8条省略...</span></span><br><span class="line">]</span><br></pre></td></tr></table></figure>

<p>提供一个编译好的安装包：<br><a href="https://codz.me/d/opentsdb-2.4.0_all.deb" target="_blank" rel="noopener">Debian package: opentsdb-2.4.0_all.deb</a></p>
<p><a href="https://gofile.io/?c=ICGijr" target="_blank" rel="noopener">Debian package: opentsdb-2.4.0_all.deb Backup Url</a></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&gt;md5sum opentsdb-2.4.0_all.deb</span><br><span class="line">22c98200d548b04ef8048922c9886cd0  opentsdb-2.4.0_all.deb</span><br></pre></td></tr></table></figure>

<h2 id="推荐一个类库"><a href="#推荐一个类库" class="headerlink" title="推荐一个类库"></a>推荐一个类库</h2><p>推荐一个<code>OpenTSDB</code>Java类库<a href="https://github.com/laudukang/opentsdb-okhttp-client" target="_blank" rel="noopener">OpenTSDB Java Async Client</a>，封装了一下几个方法，开箱即用：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">default</span> String <span class="title">buildUrl</span><span class="params">(String serviceUrl, String postApiEndPoint, ExpectResponse expectResponse)</span></span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function"><span class="keyword">default</span> String <span class="title">pushDataPointsString</span><span class="params">(DataPointBuilder builder, ExpectResponse expectResponse)</span></span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function"><span class="keyword">default</span> String <span class="title">pushQueriesString</span><span class="params">(QueryBuilder builder, ExpectResponse expectResponse)</span></span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function"><span class="keyword">default</span> <span class="keyword">void</span> <span class="title">asyncPushDataPoints</span><span class="params">(DataPointBuilder builder, ExpectResponse expectResponse, Callback callback)</span></span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function"><span class="keyword">default</span> QueryResponse <span class="title">pushQueries</span><span class="params">(QueryBuilder builder, ExpectResponse expectResponse)</span></span></span><br></pre></td></tr></table></figure>

<p>Spring boot init <code>OpenTSDBService</code> example:</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> OpenTSDBService <span class="title">openTSDBService</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    List&lt;String&gt; openTSDBServerList = configProperties.getOpentsdbServer();</span><br><span class="line">    <span class="keyword">if</span> (CollectionUtils.isEmpty(openTSDBServerList)) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(<span class="string">"empty opentsdb server url"</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> () -&gt; openTSDBServerList;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>Thanks to <a href="https://www.jetbrains.com/?from=OpenTSDB%20Java%20Async%20Client" target="_blank" rel="noopener">JetBrains</a>‘ support</p>
<img src="/2019/11/20/opentsdb-query-optimization/jetbrains_support.png" alt="jetbrains_support.png" title="">

<h2 id="部分资料"><a href="#部分资料" class="headerlink" title="部分资料"></a>部分资料</h2><ul>
<li><a href="https://github.com/laudukang/opentsdb" target="_blank" rel="noopener">laudukang/opentsdb</a></li>
<li><a href="https://github.com/laudukang/opentsdb-okhttp-client" target="_blank" rel="noopener">laudukang/opentsdb-okhttp-client</a></li>
<li><a href="http://opentsdb.net/" target="_blank" rel="noopener">OpenTSDB</a></li>
</ul>

    </div>

    
    
    

      <footer class="post-footer">
          <div class="post-tags">
              <a href="/tags/opentsdb/" rel="tag"># opentsdb</a>
          </div>

        


        
    <div class="post-nav">
      <div class="post-nav-item">
    <a href="/2018/05/07/build-spark-cluster/" rel="prev" title="Build Spark Cluster">
      <i class="fa fa-chevron-left"></i> Build Spark Cluster
    </a></div>
      <div class="post-nav-item"></div>
    </div>
      </footer>
    
  </article>
  
  
  

  </div>


          </div>
          

<script>
  window.addEventListener('tabs:register', () => {
    let activeClass = CONFIG.comments.activeClass;
    if (CONFIG.comments.storage) {
      activeClass = localStorage.getItem('comments_active') || activeClass;
    }
    if (activeClass) {
      let activeTab = document.querySelector(`a[href="#comment-${activeClass}"]`);
      if (activeTab) {
        activeTab.click();
      }
    }
  });
  if (CONFIG.comments.storage) {
    window.addEventListener('tabs:click', event => {
      if (!event.target.matches('.tabs-comment .tab-content .tab-pane')) return;
      let commentClass = event.target.classList[1];
      localStorage.setItem('comments_active', commentClass);
    });
  }
</script>

        </div>
          
  
  <div class="toggle sidebar-toggle">
    <span class="toggle-line toggle-line-first"></span>
    <span class="toggle-line toggle-line-middle"></span>
    <span class="toggle-line toggle-line-last"></span>
  </div>

  <aside class="sidebar">
    <div class="sidebar-inner">

      <ul class="sidebar-nav motion-element">
        <li class="sidebar-nav-toc">
          文章目录
        </li>
        <li class="sidebar-nav-overview">
          站点概览
        </li>
      </ul>

      <!--noindex-->
      <div class="post-toc-wrap sidebar-panel">
          <div class="post-toc motion-element"><ol class="nav"><li class="nav-item nav-level-2"><a class="nav-link" href="#OpenTSDB-简介"><span class="nav-number">1.</span> <span class="nav-text">OpenTSDB 简介</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#项目背景"><span class="nav-number">2.</span> <span class="nav-text">项目背景</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#查询瓶颈"><span class="nav-number">3.</span> <span class="nav-text">查询瓶颈</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#着手解决"><span class="nav-number">4.</span> <span class="nav-text">着手解决</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#编译测试"><span class="nav-number">5.</span> <span class="nav-text">编译测试</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#推荐一个类库"><span class="nav-number">6.</span> <span class="nav-text">推荐一个类库</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#部分资料"><span class="nav-number">7.</span> <span class="nav-text">部分资料</span></a></li></ol></div>
      </div>
      <!--/noindex-->

      <div class="site-overview-wrap sidebar-panel">
        <div class="site-author motion-element" itemprop="author" itemscope itemtype="http://schema.org/Person">
    <img class="site-author-image" itemprop="image" alt="laudukang"
      src="/images/head.png">
  <p class="site-author-name" itemprop="name">laudukang</p>
  <div class="site-description" itemprop="description">Bug Not Found</div>
</div>
<div class="site-state-wrap motion-element">
  <nav class="site-state">
      <div class="site-state-item site-state-posts">
          <a href="/archives/">
        
          <span class="site-state-item-count">68</span>
          <span class="site-state-item-name">日志</span>
        </a>
      </div>
      <div class="site-state-item site-state-categories">
            <a href="/categories/">
          
        <span class="site-state-item-count">10</span>
        <span class="site-state-item-name">分类</span></a>
      </div>
      <div class="site-state-item site-state-tags">
            <a href="/tags/">
          
        <span class="site-state-item-count">25</span>
        <span class="site-state-item-name">标签</span></a>
      </div>
  </nav>
</div>
  <div class="links-of-author motion-element">
      <span class="links-of-author-item">
        <a href="https://github.com/laudukang" title="GitHub → https:&#x2F;&#x2F;github.com&#x2F;laudukang" rel="noopener" target="_blank"><i class="fa fa-fw fa-github"></i>GitHub</a>
      </span>
      <span class="links-of-author-item">
        <a href="https://gitee.com/laudukang" title="Gitee → https:&#x2F;&#x2F;gitee.com&#x2F;laudukang" rel="noopener" target="_blank"><i class="fa fa-fw fa-github-alt"></i>Gitee</a>
      </span>
      <span class="links-of-author-item">
        <a href="https://stackoverflow.com/users/5621049/laudukang" title="StackOverflow → https:&#x2F;&#x2F;stackoverflow.com&#x2F;users&#x2F;5621049&#x2F;laudukang" rel="noopener" target="_blank"><i class="fa fa-fw fa-stack-overflow"></i>StackOverflow</a>
      </span>
      <span class="links-of-author-item">
        <a href="https://www.google.com/maps/place/%E6%B7%B1%E5%9C%B3%E5%B8%82%E8%BD%AF%E4%BB%B6%E4%BA%A7%E4%B8%9A%E5%9F%BA%E5%9C%B0/@22.5244896,113.9382973,17z/data=!3m1!4b1!4m5!3m4!1s0x3403ee17d008a019:0xbb7b06b73d856c14!8m2!3d22.5244896!4d113.9382973" title="Map → https:&#x2F;&#x2F;www.google.com&#x2F;maps&#x2F;place&#x2F;%E6%B7%B1%E5%9C%B3%E5%B8%82%E8%BD%AF%E4%BB%B6%E4%BA%A7%E4%B8%9A%E5%9F%BA%E5%9C%B0&#x2F;@22.5244896,113.9382973,17z&#x2F;data&#x3D;!3m1!4b1!4m5!3m4!1s0x3403ee17d008a019:0xbb7b06b73d856c14!8m2!3d22.5244896!4d113.9382973" rel="noopener" target="_blank"><i class="fa fa-fw fa-map-marker"></i>Map</a>
      </span>
  </div>



      </div>

    </div>
  </aside>
  <div id="sidebar-dimmer"></div>


      </div>
    </main>

    <footer class="footer">
      <div class="footer-inner">
        

<div class="copyright">
  
  &copy; 2015 – 
  <span itemprop="copyrightYear">2025</span>
  <span class="with-love">
    <i class="fa fa-heart icon-next-heart"></i>
  </span>
  <span class="author" itemprop="copyrightHolder">laudukang</span>
</div>
  <div class="powered-by">由 <a href="https://hexo.io/" class="theme-link" rel="noopener" target="_blank">Hexo</a> 强力驱动 v4.2.0
  </div>
  <span class="post-meta-divider">|</span>
  <div class="theme-info">主题 – <a href="https://muse.theme-next.org/" class="theme-link" rel="noopener" target="_blank">NexT.Muse</a> v7.7.1
  </div>

        
<div class="busuanzi-count">
  <script pjax async src="https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
</div>






  <script pjax>
  function leancloudSelector(url) {
    url = encodeURI(url);
    return document.getElementById(url).querySelector('.leancloud-visitors-count');
  }
  if (CONFIG.page.isPost) {
    function addCount(Counter) {
      var visitors = document.querySelector('.leancloud_visitors');
      var url = decodeURI(visitors.getAttribute('id'));
      var title = visitors.getAttribute('data-flag-title');

      Counter('get', `/classes/Counter?where=${JSON.stringify({ url })}`)
        .then(response => response.json())
        .then(({ results }) => {
          if (results.length > 0) {
            var counter = results[0];
              leancloudSelector(url).innerText = counter.time + 1;
            Counter('put', '/classes/Counter/' + counter.objectId, { time: { '__op': 'Increment', 'amount': 1 } })
              .then(response => response.json())
              .catch(error => {
                console.error('Failed to save visitor count', error);
              })
          } else {
              Counter('post', '/classes/Counter', { title: title, url: url, time: 1 })
                .then(response => response.json())
                .then(() => {
                  leancloudSelector(url).innerText = 1;
                })
                .catch(error => {
                  console.error('Failed to create', error);
                });
          }
        })
        .catch(error => {
          console.error('LeanCloud Counter Error', error);
        });
    }
  } else {
    function showTime(Counter) {
      var visitors = document.querySelectorAll('.leancloud_visitors');
      var entries = [...visitors].map(element => {
        return decodeURI(element.getAttribute('id'));
      });

      Counter('get', `/classes/Counter?where=${JSON.stringify({ url: { '$in': entries } })}`)
        .then(response => response.json())
        .then(({ results }) => {
          if (results.length === 0) {
            document.querySelectorAll('.leancloud_visitors .leancloud-visitors-count').forEach(element => {
              element.innerText = 0;
            });
            return;
          }
          for (let item of results) {
            let { url, time } = item;
            leancloudSelector(url).innerText = time;
          }
          for (let url of entries) {
            var element = leancloudSelector(url);
            if (element.innerText == '') {
              element.innerText = 0;
            }
          }
        })
        .catch(error => {
          console.error('LeanCloud Counter Error', error);
        });
    }
  }

  fetch('https://app-router.leancloud.cn/2/route?appId=CPfRDhOlTyfkHW155W3Kx3H1-gzGzoHsz')
    .then(response => response.json())
    .then(({ api_server }) => {
      var Counter = (method, url, data) => {
        return fetch(`https://${api_server}/1.1${url}`, {
          method: method,
          headers: {
            'X-LC-Id': 'CPfRDhOlTyfkHW155W3Kx3H1-gzGzoHsz',
            'X-LC-Key': 'TX05RQSQMd8fbL7RaJNADS7S',
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(data)
        });
      };
      if (CONFIG.page.isPost) {
        if (CONFIG.hostname !== location.hostname) return;
        addCount(Counter);
      } else if (document.querySelectorAll('.post-title-link').length >= 1) {
        showTime(Counter);
      }
    });
  </script>


      </div>
    </footer>
  </div>

  
  <script src="/lib/anime.min.js"></script>
  <script src="/lib/pjax/pjax.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js"></script>
  <script src="//cdn.jsdelivr.net/gh/fancyapps/fancybox@3/dist/jquery.fancybox.min.js"></script>
  <script src="/lib/velocity/velocity.min.js"></script>
  <script src="/lib/velocity/velocity.ui.min.js"></script>

<script src="/js/utils.js"></script>

<script src="/js/motion.js"></script>


<script src="/js/schemes/muse.js"></script>


<script src="/js/next-boot.js"></script>

  <script>
var pjax = new Pjax({
  selectors: [
    'head title',
    '#page-configurations',
    '.content-wrap',
    '.post-toc-wrap',
    '#pjax'
  ],
  switches: {
    '.post-toc-wrap': Pjax.switches.innerHTML
  },
  analytics: false,
  cacheBust: false,
  scrollTo : !CONFIG.bookmark.enable
});

window.addEventListener('pjax:success', () => {
  document.querySelectorAll('script[pjax], script#page-configurations, #pjax script').forEach(element => {
    var code = element.text || element.textContent || element.innerHTML || '';
    var parent = element.parentNode;
    parent.removeChild(element);
    var script = document.createElement('script');
    if (element.id) {
      script.id = element.id;
    }
    if (element.className) {
      script.className = element.className;
    }
    if (element.type) {
      script.type = element.type;
    }
    if (element.src) {
      script.src = element.src;
      // Force synchronous loading of peripheral JS.
      script.async = false;
    }
    if (element.getAttribute('pjax') !== null) {
      script.setAttribute('pjax', '');
    }
    if (code !== '') {
      script.appendChild(document.createTextNode(code));
    }
    parent.appendChild(script);
  });
  NexT.boot.refresh();
  // Define Motion Sequence & Bootstrap Motion.
  if (CONFIG.motion.enable) {
    NexT.motion.integrator
      .init()
      .add(NexT.motion.middleWares.subMenu)
      .add(NexT.motion.middleWares.postList)
      .bootstrap();
  }
  NexT.utils.updateSidebarPosition();
});
</script>




  















    <div id="pjax">
  

  
<script>window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };</script><script defer src="/_vercel/insights/script.js" data-endpoint="/this-is-vercel-insights"></script><script>window.si = window.si || function () { (window.siq = window.siq || []).push(arguments); };</script><script defer src="/_vercel/speed-insights/script.js" data-endpoint="/this-is-vercel-insights-speed"></script>
    </div>
</body>
</html>
